Add ad-hoc AI credit pack option to onboarding "Choose how to start" - #14605
Add ad-hoc AI credit pack option to onboarding "Choose how to start"#14605warp-agent-staging[bot] wants to merge 10 commits into
Conversation
Free users who don't want a subscription had no way to get AI credits during onboarding: the "Choose how to start" offer only forked between "Use Warp with AI" (subscribe) and "Set up AI later". Adds a third option that buys a one-time add-on credit pack: - `OfferChoice::BuyCredits` renders a card with the server's credit packs (credits, premium-adjusted price, volume savings), keyboard navigable alongside the other two options. - Pack prices come from `pricingInfo.addonCreditsOptions` plus the viewer's `PurchaseAddOnCreditsPolicy` premium at runtime; nothing is hardcoded, and the option is hidden when pricing is unavailable. - "Get Warping" on that option calls `purchaseAddonCredits` with no `teamUid`, so the server resolves or creates a personal team. - A checkout hand-off opens the URL and leaves the user on the slide; onboarding advances only once the granted credits are observed on a usage refresh, so abandoning checkout never advances anyone. - The subscribe card now states the savings-framed add-on discount. The onboarding demo binary can now preview the post-auth offer slides via ONBOARDING_DEMO_OFFER, and initializes feature flags so its debug build can render them at all. Co-Authored-By: Warp <agent@warp.dev> Co-Authored-By: Oz <oz-agent@warp.dev>
The post-auth offer slide is only constructed on the account-first
onboarding flow, so previewing it from the demo binary panicked at the
`expect("offer slide exists")` in the render match. Turn the flag on
when ONBOARDING_DEMO_OFFER selects an offer to preview.
Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
…boarding-credit-packs
Requester feedback on the onboarding offer slide: - "Use Warp with AI" no longer distinguishes the card now that two of the three options are ways to use Warp with AI, so it is now "Subscribe to a Warp plan". The telemetry action name is deliberately unchanged so existing dashboards keep continuity. - The stacked pack rows made the slide tall enough to scroll. The four packs now sit on a single horizontal row of equal-width tiles, following how the Billing & Usage page presents add-on credit denominations (compact credit chips 8px apart, with the credits icon). Each tile keeps its credit count, premium-adjusted price and savings badge; the count drops the redundant "credits" suffix, which the card title and icon already supply. Co-Authored-By: Warp <agent@warp.dev> Co-Authored-By: Oz <oz-agent@warp.dev>
The checkout-pending onboarding state advanced whenever `has_any_ai_remaining()` became true. That predicate is broader than "the pack the user just tried to buy was granted" — it is also true for base plan requests, BYOK credentials, overages and auto-reload. A user who already had AI availability could open checkout, cancel, and still be advanced, which breaks the requirement that abandoning checkout leaves them on the slide. `AwaitingCheckout` now carries the purchased-credit balance observed just before checkout opened, and the new `on_credit_balance_observed` completes the purchase only when that balance has actually grown. The app supplies the balance from the user- and workspace-scoped bonus grants that a pack purchase lands in, rather than any-AI-availability. Co-Authored-By: Warp <agent@warp.dev> Co-Authored-By: Oz <oz-agent@warp.dev>
…boarding-credit-packs
Four adjustments from the PR review: - The subscribe card's add-on savings line only makes sense beside the packs it refers to, so `primary_description` now takes the same `shows_credit_packs` condition that decides whether the buy-credits card renders, and falls back to the original copy otherwise. Reusing that one condition avoids a parallel predicate that could drift. - The transient "Starting your purchase…" and "Finish your purchase in the browser…" lines are gone, along with their strings; the "Waiting for checkout…" button label is the only status the user sees. The failure line stays: it is terminal rather than transient, and without it a rejected purchase would fail silently. - Every pack tile now renders a badge slot so all four are the same height. The smallest pack has no volume discount, so its slot lays out the same text fully transparent, reserving the right line box without a fixed-height constant. - The advance-after-checkout rule is relaxed from "the balance grew" to "the balance is non-zero". A brand-new account with no credits who cancels still stays on the slide, while someone who already holds credits can continue. The balance stays scoped to purchased add-on credits rather than general AI availability, which would also count base free-plan requests and BYOK and so would advance a brand-new user who cancelled. Co-Authored-By: Warp <agent@warp.dev> Co-Authored-By: Oz <oz-agent@warp.dev>
…boarding-credit-packs
| /// Env var for jumping straight to a post-auth offer slide, which is otherwise | ||
| /// only reachable from the app after authentication. Accepts | ||
| /// `choose_how_to_start` or `head_start`. | ||
| const DEMO_OFFER_ENV: &str = "ONBOARDING_DEMO_OFFER"; |
There was a problem hiding this comment.
Is this only used in local testing of the crate or something? Feels very strange to leave in the final product, but I guess if it's not used as part of the app crate then I don't care too much
There was a problem hiding this comment.
Local-only, yes — and it never reaches the product. crates/onboarding declares the binary with required-features = ["bin"], nothing in the workspace enables that feature by default, and the warp app crate depends on onboarding as a library only, so src/bin/main.rs isn't compiled into the app or any bundle. You have to opt in explicitly:
cargo run -p onboarding --bin onboarding --features bin
ONBOARDING_DEMO_OFFER=choose_how_to_start cargo run -p onboarding --bin onboarding --features bin
The env var exists because the post-auth offer slides are only reachable after real authentication, so without it there's no way to look at this slide in the demo harness at all — which is how I got the screenshots on this PR. It's read only by the demo binary; the app has no code path that looks at ONBOARDING_DEMO_OFFER.
Leaving this thread open in case you'd still rather it went away. If "not in the app crate" is enough for you I'll resolve it; if you want it gone I'd rather replace it with a keyboard shortcut in the demo harness than delete the capability, since previewing post-auth slides is otherwise impossible without a server.
…boarding-credit-packs
…ility Review feedback from the PR: - Move the onboarding credit helpers out of `root_view` into `app/src/ai/onboarding.rs`, next to the other onboarding AI helpers. - Gate the post-checkout advance on the server's `aiCreditAvailability` decision instead of reconstructing a purchased-credit balance on the client. Onboarding only needs to know the user can make an AI request. - Bill the purchase to the team the window is scoped to instead of hardcoding `None`, so team-discovery/domain-capture signups resolve. - Drop the extra `refresh_request_usage` on app re-activation now that availability rides along on the workspace-metadata query. - Comment wording fixes in `root_view` and the onboarding demo binary. Co-Authored-By: Oz <oz-agent@warp.dev>
Summary
Free users who don't want a subscription had no way to get AI credits during onboarding. The Choose how to start offer (
OfferVariant::ChooseHowToStart) only forked between subscribing and "Set up AI later", so anyone who just wanted to try Warp had to either commit to a plan or start with no AI at all.This adds a third option that buys a one-time add-on credit pack.
Buy AI credits card. A new
OfferChoice::BuyCreditssits between the subscribe and skip cards and presents the server's credit packs as a single horizontal row of tiles — credit count, the price actually charged, and the volume savings vs the smallest pack. Tiles are click-selectable and the three cards are arrow-key navigable. The layout follows the Billing & Usage page's add-on credit denominations row (compact credit chips 8px apart, with the same credits icon); the tiles areExpandedso the packs always stay on one line rather than wrapping.Pricing is sourced at runtime, never hardcoded.
ai::onboarding::onboarding_credit_packsbuilds the list frompricingInfo.addonCreditsOptionsand the viewer'sPurchaseAddOnCreditsPolicy::effective_premium_bps, reusingprice_usd_cents_with_premiumso the displayed price matches what the server charges (free plan:$12 / $24 / $60 / $120for the 400 / 1,000 / 3,000 / 6,500 packs). The option is hidden entirely when pricing hasn't loaded or the plan can't buy packs, and the packs refresh when pricing or the user's teams change.Purchase and checkout return. "Get Warping" on that option calls
purchaseAddonCredits, billed to whichever team the window is scoped to (usuallyNoneduring onboarding, which lets the server resolve or create the buyer's personal team). APurchaseAddonCreditsCheckoutOutputopens the checkout URL and leaves the user on the slide; onboarding advances once the server'saiCreditAvailabilitydecision says the user can actually make an AI request. That is the whole bar — onboarding doesn't care how the user got access, only that it never lets through someone who still can't use AI. Because this offer is only shown to users with no base credits, a brand-new account that cancels checkout keeps reporting unavailable and stays on the slide. A synchronous charge advances immediately; a rejection surfaces a retryable error.Copy. The subscribe card is titled "Subscribe to a Warp plan". When the credit packs are on screen it adds the savings-framed line save 20% on add-on credits with any Build plan (matching the web wording, no "surcharge" language); when they aren't, it keeps its original copy.
Demo binary.
crates/onboarding's standalone demo can now preview the post-auth offer slides viaONBOARDING_DEMO_OFFER=choose_how_to_start|head_start. It also had to start initializing feature flags — a debug build previously panicked on the firstFeatureFlag::is_enabledcheck before any window rendered, so the demo was unusable for these slides.Scoping note on
ai_access_slideTriage flagged whether "Get AI access" (
ai_access_slide.rs) needs the same third option. It does not, and this PR deliberately leaves it alone: that slide is only constructed on the non-account-first path (agent_onboarding_view.rsbuildsai_access_slidewhenAccountFirstOnboardingis disabled andoffer_slidewhen it is enabled), so the two are alternative flows rather than forks that must stay in sync.Rework changes
Round 1 — requester feedback on the first implementation.
use_warp_with_ai) is deliberately not renamed so existing dashboards keep continuity — called out in a code comment."credits"word after each number, soCreditPackOption::credits_label()returns just the thousands-separated count ("6,500").Round 2 — review finding on the advance predicate.
has_any_ai_remaining()became true, which is broader than "the pack the user bought was granted", so a user with pre-existing AI availability could cancel checkout and still be advanced. The check moved to the purchased/add-on credit balance.Round 3 — requester feedback on this PR (current).
primary_descriptionnow takes the sameshows_credit_packscondition that decides whether the buy-credits card renders, rather than a second predicate that could drift, and falls back to exactly the previous copy when the packs aren't shown.has_any_ai_remaining, so a brand-new zero-credit user who cancels still stays put while someone who already has credits can continue.canceled_checkout_does_not_advance_a_user_who_already_had_creditsasserted the now-unwanted behavior and was replaced bycanceled_checkout_does_not_advance_a_user_with_no_creditsplusa_user_who_already_has_credits_can_continue.Round 4 — requester review comments on this PR (current). Plumbing and comments only; nothing on screen changes, so no new visual proof was captured.
aiCreditAvailability, not a client-side balance. [REV-1714] Use server-authoritative AI credit availability in the client #14634 landed on master while this was in review; that branch is merged in andpurchased_credit_balance()is deleted. The subscription listens forAIRequestUsageModelEvent::CreditAvailabilityUpdatedand readsAIRequestUsageModel::server_availability().available.on_purchased_credit_balance_observed(i32)→on_ai_credit_availability_observed(bool)on the view,on_credit_balance_observed→on_credit_availability_observed(bool)on the model. This also fixes a case the balance version got wrong: access arriving from a team plan mid-checkout now completes.root_viewintoapp/src/ai/onboarding.rs, next to the other onboarding AI helpers:onboarding_credit_packs,onboarding_purchase_team_uidandhas_ai_credit_availability.root_viewkeeps only the subscriptions and the event relay.UserWorkspaces::team_uid_for_windowinstead of a hardcodedNone, so team-discovery and domain-capture signups resolve to the team they landed on.refresh_request_usageon app re-activation is removed — availability rides along on the workspace-metadata query that the same handler already refreshes.ONBOARDING_DEMO_OFFERenv var should stay. It's gated behindrequired-features = ["bin"]and never compiled into the app.Verification
Regression tests (new; the pricing math test is the guard on the confirmed server SKUs):
app/src/pricing/pricing_tests.rs— 5 tests pinning the premium math ($10/$20/$50/$100at list,$12/$24/$60/$120at 2000 bps), volume savings0/20/33/38%, empty input, and the no-negative-savings edge case.crates/onboarding/src/model_tests.rs— 11 tests over the purchase state machine, including the cancelled-checkout case (repeated unavailable answers hold the slide), access arriving from any source completing the purchase, and that the availability callback is inert with no checkout pending.crates/onboarding/src/slides/offer_slide_tests.rs— 7 tests over the slide: the third option appears only for the free-standard offer and only with packs, the subscribe copy with and without packs, arrow-key navigation,get_warping_buys_credits_when_the_credit_option_is_selected, "Set up AI later" still emitting while checkout is pending, and pack-list truncation.Repository checks:
./script/format;cargo clippy -p onboarding --all-targets --tests -- -D warnings;cargo clippy -p warp --all-targets --tests -- -D warnings;cargo test -p onboarding(44 passed);cargo test -p warp --lib pricing::(5 passed);cargo test -p warp --lib root_view(11 passed). All re-run after this rework and after merging the currentmasterinto this branch.Visual verification (full-desktop screenshots and a recording below), run against the real UI via
ONBOARDING_DEMO_OFFER=choose_how_to_start ./target/debug/onboarding:400 / $12,1,000 / $24Save 20%,3,000 / $60Save 33%,6,500 / $120Save 38% — and all four tile boxes are now the same height, with the400tile's blank badge slot bringing its bottom border level with the others.subscribe_copy_drops_the_add_on_line_when_no_packs_are_showninstead.Originating thread: https://warpdev.slack.com/archives/C0BDQDW8V5E/p1785523238898989
Computer-use screenshots
Full-desktop, uncropped captures with the app window surrounded by desktop, so the absence of clipping and of any scrollbar is visible rather than asserted. The demo runs under Xvfb with no window manager, so the window has no OS title bar and cannot be resized; the surrounding desktop and taskbar show its full extent.
Computer-use video recording
Continuous full-screen recording: default state, starting a purchase via the primary button, and selecting the last credit pack.
Conversation: https://staging.warp.dev/conversation/0bec429a-a2ca-46af-892b-4ce333e400ee
Run: https://oz.staging.warp.dev/runs/019fb990-9952-7194-b171-5995e49cc9de
This PR was generated with Oz.